<?php

// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2014 The Herosphp Authors. All rights reserved.
// * Use of this source code is governed by a MIT-style license
// * that can be found in the LICENSE file.
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
declare(strict_types=1);

namespace herosphp\core;

use herosphp\exception\SessionException;
use herosphp\GF;
use herosphp\session\Session;
use herosphp\session\SessionError;
use Workerman\Protocols\Http\Request;
use Workerman\Worker;

/**
 * web http request wrapper class
 * @author RockYang<yangjian102621@gmail.com>
 */
class HttpRequest extends Request
{
    // middleware data store key
    const META_DATA_KEY = '__META_DATA_KEY__';

    // Session instance
    protected ?Session $_session = null;

    protected ?Session $_usession = null;


    /**
     * Get session
     * You should pass the $userId for user login
     * {@inheritdoc}
     */
    public function session(mixed $uid = null): Session
    {
        if ($uid === null) {
            if ($this->_session === null) {
                $session = new Session(Session::NAME, $this->connection);
                $this->startSession($session, $uid, Session::NAME);
                $this->_session = $session;
            }
            return $this->_session;
        }
        if ($this->_usession === null) {
            $session = new Session(Session::UNAME, $this->connection);
            $this->startSession($session, $uid, Session::UNAME);
            $this->_usession = $session;
        }

        return $this->_usession;
    }

    // start Session
    public function startSession(Session $session, mixed $uid, string $sessionName): void
    {
        if ($this->connection === null) {
            Worker::safeEcho('Request->session() fail, header already send');
            throw new SessionException(SessionError::ERR_LOSE_CONNECT);
        }

        $sessConfig = Session::$config;
        $addr = $this->connection->getRemoteIp();
        $device = $this->header('user-agent');
        $config = Config::get('session');
        // old session
        $sessToken = $this->get($sessionName) ?? $this->post($sessionName) ??
            $this->header($sessionName) ?? $this->cookie($sessionName);
        if ($sessToken) {
            $token = @json_decode(@base64_decode($sessToken), true);
            if (empty($token)) {
                throw new SessionException(SessionError::ERR_INVALID_SESS_TOKEN);
            }

            // check the token keys
            // format: {"uid":"xxx","seed":"xxx","addr":"xxx","sign":"xxx"}
            foreach (['uid', 'seed', 'addr', 'sign'] as $val) {
                if (!isset($token[$val])) {
                    throw new SessionException(SessionError::ERR_INVALID_SESS_TOKEN);
                }
            }

            // check token sign
            $sign = Session::buildSign((string)$token['uid'], $token['seed'], $token['addr']);
            if (strcmp($sign, $token['sign']) !== 0) {
                throw new SessionException(SessionError::ERR_INVALID_SESS_TOKEN);
            }

            // check if the ip address is changed
            if ($config['strict_mode'] && $addr != $token['addr']) {
                throw new SessionException(SessionError::ERR_ADDR_CHANGED);
            }

            // not the same user
            if ($uid != null && $uid != $token['uid']) {
                throw new SessionException(SessionError::ERR_INVALID_UID);
            }

            $uid = $token['uid'];
            $seed = $token['seed'];
        } else { // create new session
            if ($uid === null) {
                $uid = static::createSessionId();
            }
            $seed = sprintf('%.6f', microtime(true));
            // build session token
            $sign = Session::buildSign((string)$uid, $seed, $addr);
            $sessToken = base64_encode(json_encode(['uid' => $uid, 'seed' => $seed, 'addr' => $addr, 'sign' => $sign]));
        }

        $sessionId = sha1($uid . $sessConfig['private_key']);
        $session->start($seed, $sessionId, $uid, $sessToken);

        // check if the old session is valid
        $client = $session->getClient($seed);
        if ($client) {
            // check if the client is offline
            if ($client['status'] === Session::C_STATUS_OFF) {
                // remove client
                $session->removeClient($seed);
                throw new SessionException(SessionError::ERR_PUSHED_OFFLINE);
            }

            // check if the device change
            if ($config['strict_mode'] && strcmp($client['device'], $device) !== 0) {
                throw new SessionException(SessionError::ERR_DEVICE_CHANGED);
            }
        } else {
            // register a new client device
            $session->addClient([
                'uid' => $uid,
                'addr' => $addr,
                'device' => $device,
                'status' => Session::C_STATUS_OK,
                'created-at' => time()
            ]);
        }
    }

    /**
     * @return bool
     */
    public function expectsJson(): bool
    {
        return ($this->isAjax() && !$this->isPjax()) || $this->acceptJson();
    }

    /**
     * @return bool
     */
    public function isAjax(): bool
    {
        return $this->header('X-Requested-With') === 'XMLHttpRequest';
    }

    /**
     * @return bool
     */
    public function isPjax(): bool
    {
        return (bool)$this->header('X-PJAX');
    }

    /**
     * @return bool
     */
    public function acceptJson(): bool
    {
        return str_contains($this->header('accept', ''), 'json');
    }

    // store middleware data
    public function putMetaData(string $name, mixed $value)
    {
        if (!isset($this->_data[static::META_DATA_KEY])) {
            $this->_data[static::META_DATA_KEY] = [];
        }

        $this->_data[static::META_DATA_KEY][$name] = $value;
    }

    // get middleware data
    public function getMetaData(string $name, $default = null): mixed
    {
        if (!isset($this->_data[static::META_DATA_KEY])) {
            return $default;
        }

        return $this->_data[static::META_DATA_KEY][$name] ?? $default;
    }

    // get all middleware data
    public function getAllMetaData(): array
    {
        return $this->_data[static::META_DATA_KEY] ?? [];
    }

    /**
     * get real ip
     * @param bool $safeMode
     * @return string
     */
    public function getRealIp(bool $safeMode = true): string
    {
        $remoteIp = $this->connection->getRemoteIp();
        if ($safeMode && !GF::isIntranetIp($remoteIp)) {
            return $remoteIp;
        }
        return $this->header('client-ip', $this->header(
            'x-forwarded-for',
            $this->header('x-real-ip', $this->header(
                'x-client-ip',
                $this->header('via', $remoteIp)
            ))
        ));
    }

    // support XSS过滤
    public function getParameter(string $name, mixed $default = null, string $processor = 'trim')
    {
        if (!isset($this->_data['get'])) {
            $this->parseGet();
        }
        if (!isset($this->_data['post'])) {
            $this->parsePost();
        }
        $params = array_merge($this->_data['get'], $this->_data['post']);
        if (isset($params[$name])) {
            $value = $params[$name];
            if ($processor != '' && !is_array($value)) {
                $functions = explode('|', $processor);
                foreach ($functions as $func) {
                    $value = call_user_func($func, $value);
                }
            }
            return $value;
        }
        return $default;
    }

    // get all params
    public function all(): array
    {
        return array_merge($this->_data['get'], $this->_data['post']);
    }

    // filter value
    private function filter(mixed $value)
    {
        if (is_array($value)) {
            array_walk_recursive($value, function (&$item) {
                if (is_string($item)) {
                    $item = htmlspecialchars($item);
                }
            });
        } else {
            $value = htmlspecialchars($value);
        }
        return $value;
    }

    // support workerMan session
    public function simpleSession(): bool|\Workerman\Protocols\Http\Session
    {
        return parent::session();
    }
}
